"""The module used to request for recommendations"""

from readers import Profile

import json

from twisted.internet import reactor
from twisted.internet.defer import Deferred, AlreadyCalledError
from twisted.internet.protocol import ClientFactory
from twisted.protocols.basic import LineReceiver

class GetRecommendedProfile(object):
    """Each instance of this is the action of obtaining the recommended
    songs"""
    def __init__(self, elements, peers, profile, profileName,
                 callbackFunction):
        """Calls the callback function on the profile generated of recommended
        elements"""
        self.__elements=elements
        self.__profile=profile
        topElements=self.findTopElements(elements)
        reducedElements=self.reduceElements(topElements, profile.uniqueAtts)
        #This has to be checked for connection
        dPeer=FindBestPeer(peers, reducedElements, profileName).dPeer
        dElements=RequestRecommmended(dPeer, profileName).dElements
        dElements.addCallback(self.subtractElements)
        dElements.addCallback(self.formProfile)
        dElements.addCallback(callbackFunction)
    @staticmethod
    def findTopElements(elements, ratingAtt='Rating'):
        """Takes only those elements with an above average rating"""
        average=sum(int(element[ratingAtt]) for element in elements)/len(elements)
        return (element for element in elements
                if int(element[ratingAtt])>average)
    @staticmethod
    def reduceElements(elements, uniqueAtts):
        """Reduces the elements to only those attributes which are unique to
        it"""
        return (dict((att, element[att]) for att in uniqueAtts)
                for element in elements)
    def subtractElements(self, elements):
        """Removes all of the elements already held"""
        #This would be easier and probably faster if you make all of these
        #into sets with the uniqueAtts giving the hashFunction
        return (nElement for nElement in elements
                if not any(all(element[att]==nElement[att] for att in
                               self.__profile.uniqueAtts)
                           for element in self.__elements))
    def formProfile(self, elements):
        print 'profile formed'
        return Profile(elements, self.__profile.atts, '',
                       self.__profile.uniqueAtts)

class FindBestPeer(object):
    """Each instance of this is an attempt to find the best peer with the
    given values"""
    def __init__(self, peers, reducedElements, profileName):
        for peer in peers:
            peerScorer=ScorerFactory(peer, reducedElements, profileName)
            peerScorer.dScore.addCallback(self.record)
            peerScorer.connect()
        self.dPeer=Deferred()
        self.__peersLeft=len(peers)
        self.__maxScore=-1
        self.__bestPeer=None
    def record(self, peerAndScore):
        """Records a peer and score until all have been found, at which point
        the deferred is triggered with it"""
        peer, score=peerAndScore
        print peer['I.P.'], score
        if score is not None and (score>self.__maxScore or self.__bestPeer is None):
            self.__bestPeer=peer
            print 'best set'
        self.__peersLeft-=1
        if not self.__peersLeft:
            self.dPeer.callback(self.__bestPeer)
    def getBest(self):
        """Triggers the deferred with the best peer currently available"""
        try:
            self.dPeer.callback(self.__bestPeer)
        except AlreadyCalledError:
            pass

class ScorerProtocol(LineReceiver):
    """The protocol used for obtaining the score"""
    def connectionMade(self):
        self.sendLine('PROFILE: '+self.factory.profileName)
        for line in self.factory.lines:
            self.sendLine(line)
        self.sendLine('END')
    def lineReceived(self, line):
        self.factory.scoreFound(int(line))
        self.transport.loseConnection()

class ScorerFactory(ClientFactory):
    """The factory that obtains the score of a peer"""
    protocol=ScorerProtocol
    def __init__(self, peer, reducedElements, profileName):
        self.peer=peer
        self.dScore=Deferred()
        self.lines=[json.dumps(element) for element in reducedElements]
        self.profileName=profileName
    def connect(self):
        """Connects to the peer"""
        reactor.connectTCP(self.peer['I.P.'], self.peer['Scorer Port'], self)
    def scoreFound(self, score):
        """Passes up the score when it is found"""
        self.dScore.callback((self.peer, score))
    def __failure(self, connector, reason):
        """Deals with a connection failure"""
        try:
            self.dScore.callback((self.peer, None))
        except AlreadyCalledError:
            pass
    clientConnectionFailed=clientConnectionLost=__failure

class RequestProtocol(LineReceiver):
    """The protocol for a request for elements"""
    def connectionMade(self):
        self.sendLine(self.factory.profileName)
        self.receivedElements=[]
    def lineReceived(self, line):
        if line=='END':
            self.factory.dElements.callback(self.receivedElements)
            self.transport.loseConnection()
        else:
            element=json.loads(line)
            element['Location']=self.factory.peer #Remember to modify before saving the element
            self.receivedElements.append(element)
        
class RequestRecommmended(ClientFactory):
    protocol=RequestProtocol
    def __init__(self, dPeer, profileName):
        self.profileName=profileName
        dPeer.addCallback(self.request)
        self.dElements=Deferred()
    def request(self, peer):
        self.peer=peer
        reactor.connectTCP(peer['I.P.'], peer['Sender Port'], self)

